package ph.com.gs3.formalistics.services.managers;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import ph.com.gs3.formalistics.global.interfaces.CallbackCommand;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.model.HttpCommunicator;
import ph.com.gs3.formalistics.model.RemoteDataAccessObject.RDAOCommunicationException;
import ph.com.gs3.formalistics.model.facades.CompaniesDataWriter;
import ph.com.gs3.formalistics.model.facades.UsersDataWriter;
import ph.com.gs3.formalistics.model.ldao.UsersLDAO;
import ph.com.gs3.formalistics.model.rdao.UsersRDAO;
import ph.com.gs3.formalistics.model.vo.application.APIResponse;
import ph.com.gs3.formalistics.model.vo.application.APIResponse.APIInvalidResponseException;
import ph.com.gs3.formalistics.model.vo.application.APIResponse.APIParameterException;
import ph.com.gs3.formalistics.model.vo.business.Company;
import ph.com.gs3.formalistics.model.vo.business.User;
import ph.com.gs3.formalistics.model.vo.parsers.json.CompanyJSONParser;
import ph.com.gs3.formalistics.model.vo.parsers.json.UserJSONParser;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;

/**
 * 
 * Manages connectivity and session state of the application. This manager listens to
 * connectivity changes and checks for server availability every time a connection is
 * available. This serves as the Facade for session related functionalities.
 * 
 * @author Ervinne Sodusta
 */
public class SessionManager {

	public static final String TAG = SessionManager.class.getSimpleName();

	// =====================================================================
	// Constants & Enumerations

	public static class CheckConnectionResult {

		public int connectionStatus;
		public String errorMessage;

	}

	public enum LoginFields {
		SERVER, EMAIL, PASSWORD
	}

	public static enum Mode {
		OFFLINE, ONLINE
	}

	// =====================================================================
	// Server errors/exceptions
	public static final String SERVER_ERROR_USER_NOT_FOUND = "User Not Found";

	// =====================================================================
	// Fields

	protected boolean isCurrentlyCheckingServerConnection;
	protected boolean isAuthenticated;
	protected Mode mode;
	protected int connectivityType;

	private boolean isCurrentlyNotifyingListeners;
	private List<SessionManagerEventListener> listenerAddQueue;
	private List<SessionManagerEventListener> listenerRemoveQueue;

	private List<CallbackCommand<CheckConnectionResult>> checkConnectionMainThreadCallbackList;
	private List<CallbackCommand<CheckConnectionResult>> checkConnectionbackList;

	// =====================================================================
	// Manager Fields

	protected BroadcastReceiver networkChangeBroadcastReceiver;
	protected List<SessionManagerEventListener> listeners;

	// =====================================================================
	// Dependencies

	protected UsersLDAO usersLDAO;
	protected UsersRDAO usersRDAO;

	protected Context applicationContext;

	/**
	 * Default instance is created on the application, this is an improvised singleton
	 * where the instance is generated once and its dependencies are injected in the
	 * application class.
	 */
	protected static SessionManager applicationInstance;

	/**
	 * Gives the application instance created in the application class.
	 * 
	 * @return an improvised singleton instance of FLSessionManager.
	 */
	public static SessionManager getApplicationInstance() {
		return applicationInstance;
	}

	/**
	 * Creates a new instance of FLSessionManager
	 * 
	 * @param applicationContext
	 *            dependency, the application context.
	 * @param userAccountsDAO
	 *            dependency, a new data access object for user accounts.
	 * @param authCommModule
	 *            dependency, a new authentication communicator module.
	 */
	public SessionManager(Context applicationContext) {

		this.applicationContext = applicationContext;
		this.usersLDAO = new UsersLDAO(applicationContext);
		this.usersRDAO = new UsersRDAO();

		listeners = new ArrayList<SessionManagerEventListener>();
		listenerAddQueue = new ArrayList<SessionManagerEventListener>();
		listenerRemoveQueue = new ArrayList<SessionManagerEventListener>();

		isAuthenticated = false;
		isCurrentlyCheckingServerConnection = false;

		isCurrentlyNotifyingListeners = false;

		checkConnectionMainThreadCallbackList = new ArrayList<>();
		checkConnectionbackList = new ArrayList<>();

		connectivityType = -1;// no connection

	}

	/**
	 * Creates the (improvised singleton) application instance
	 * 
	 * @param applicationContext
	 * @param userAccountsDAO
	 * @param authCommModule
	 * @return
	 */
	public static SessionManager createApplicationInstance(Context applicationContext) {

		if (applicationInstance == null) {
			applicationInstance = new SessionManager(applicationContext);
		} else {
			throw new RuntimeException("A default instance of FLSessionManager is already created");
		}

		return applicationInstance;
	}

	// =====================================================================
	// Functional Methods

	public void startListeningToNetworkChanges() {
		initializeNetworkChangeBroadcastReceiver();

		IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
		applicationContext.registerReceiver(networkChangeBroadcastReceiver, intentFilter);
	}

	public User login(String server, String email, String password) throws LoginException {

		isAuthenticated = false;
		usersRDAO.setServer(server);

		CompaniesDataWriter companyDataWriter = new CompaniesDataWriter(applicationContext);
		UsersDataWriter userDataWriter = new UsersDataWriter(applicationContext);

		APIResponse serverResponse;

		try {
			// Throws CommunicatorException and/or APIResponseException
			serverResponse = usersRDAO.login(email, password);
		} catch (RDAOCommunicationException e) {
			throw new LoginException("Failed to connect to server", e);
		} catch (APIInvalidResponseException e) {
			throw new LoginException("Server gave an invalid response", e);
		} catch (APIParameterException e) {
			throw new LoginException("The parameters provided are not enough or are not complete",
			        e);
		}

		if (serverResponse.isOperationSuccessful()) {

			String rawResults = serverResponse.getResults();
			// Throws JSONException
			JSONObject rawUserJSON;
			JSONObject rawCompanyJSON;
			User user = null;
			Company userCompany = null;

			// Parse the user data

			try {
				rawUserJSON = new JSONObject(rawResults);
				rawCompanyJSON = rawUserJSON.getJSONObject("company");

				user = UserJSONParser.createFromLoginJSON(rawUserJSON);
				user.setPassword(password);	// TODO: add encryption here

				userCompany = CompanyJSONParser.createFromLoginJSON(rawCompanyJSON, server);

			} catch (JSONException e) {
				throw new LoginException("Error parsing response from server", e);
			}

			// Update the company and the user in the database
			Company registeredCompany = companyDataWriter.registerCompany(userCompany);
			user.setCompany(registeredCompany);
			user.setActive(true);
			User registeredUser = userDataWriter.registerOrUpdateUser(user);

			isAuthenticated = true;
			mode = Mode.ONLINE;
			FLLogger.d(TAG, "Successfully logged in");

			return registeredUser;
		} else {
			throw new LoginException(serverResponse);
		}

	}

	protected void initializeNetworkChangeBroadcastReceiver() {

		networkChangeBroadcastReceiver = new BroadcastReceiver() {

			@Override
			public void onReceive(Context context, Intent intent) {

				// Note: onReceive is called twice on cherry mobile flare 2x

				ConnectivityManager cm = (ConnectivityManager) context
				        .getSystemService(Context.CONNECTIVITY_SERVICE);

				NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
				boolean isConnected = activeNetwork != null
				        && activeNetwork.isConnectedOrConnecting();

				if (isConnected) {
					connectivityType = activeNetwork.getType();
					checkConnectionToServer(null, new CallbackCommand<CheckConnectionResult>() {

						@Override
						public void execute(CheckConnectionResult result) {
							String modeChangeDebugMessage = "Mode changed: ";

							if (result.connectionStatus == HttpCommunicator.STATUS_CONNECTED) {
								mode = Mode.ONLINE;
								modeChangeDebugMessage += "Online mode";
							} else if (result.connectionStatus == HttpCommunicator.STATUS_ERROR_ON_CONNECT) {
								mode = Mode.OFFLINE;
								modeChangeDebugMessage += "Offline mode, " + result.errorMessage;

								notifyListenersSessionLost();
							} else {
								mode = Mode.OFFLINE;
								modeChangeDebugMessage += "Offline mode, unable to connect to server";
							}

							FLLogger.d(TAG, modeChangeDebugMessage);

							notifyListenersModeChanged(mode);
						}

					});
				} else {
					connectivityType = -1;
					mode = Mode.OFFLINE;
					notifyListenersModeChanged(mode);

					FLLogger.d(TAG,
					        "Mode changed: Offline mode, no wifi or mobile connection available");
				}

			}

		};
	}

	/**
	 * Checks connection to the server and executes one list of callbacks from the backend
	 * thread and another one from the main thread (added through backendThreadCallback,
	 * and mainThreadCallback).
	 * 
	 * If this method is called multiple times while it's still currently checking
	 * connection, it will just add the callbacks to the list of callbacks and will be
	 * executed by batch later on after the current connection checking. The lists will be
	 * cleared after connection checking is done.
	 * 
	 * @param backendThreadCallback
	 * @param mainThreadCallback
	 */
	public synchronized void checkConnectionToServer(
	        CallbackCommand<CheckConnectionResult> backendThreadCallback,
	        CallbackCommand<CheckConnectionResult> mainThreadCallback) {

		checkConnectionbackList.add(backendThreadCallback);
		checkConnectionMainThreadCallbackList.add(mainThreadCallback);

		if (!isCurrentlyCheckingServerConnection) {
			isCurrentlyCheckingServerConnection = true;
			new CheckConnectionTask().execute();
		}

	}

	public void logout() {

		// Remove active user
		usersLDAO.open();
		usersLDAO.deactivateAllUsers();
		usersLDAO.close();

	}

	// =====================================================================
	// Utility Methods

	// protected void checkDependencies() {
	//
	// String[] dependencyClasses = { "UserAccountsDAO",
	// "AuthenticationCommunicatorModule" };
	// Object[] dependencies = { usersLDAO, authCommModule };
	// DependencyChecker.checkDependencies(dependencyClasses, dependencies);
	//
	// }

	// ============================================================================
	// Observation Methods

	protected synchronized void notifyListenersModeChanged(Mode mode) {

		isCurrentlyNotifyingListeners = true;

		for (SessionManagerEventListener listener : listeners) {
			listener.onModeChanged(mode);
		}

		isCurrentlyNotifyingListeners = false;

	}

	/**
	 * Unsupported
	 */
	protected synchronized void notifyListenersSessionLost() {

		isCurrentlyNotifyingListeners = true;

		FLLogger.d(TAG, "listener count: " + listeners.size());

		for (SessionManagerEventListener listener : listeners) {
			listener.onSessionLost();
		}

		isCurrentlyNotifyingListeners = false;

	}

	public synchronized void addListener(SessionManagerEventListener listener) {
		if (isCurrentlyNotifyingListeners) {
			listenerAddQueue.add(listener);
		} else {
			listeners.add(listener);
		}
	}

	public synchronized void removeListener(SessionManagerEventListener listener) {
		if (isCurrentlyNotifyingListeners) {
			listenerRemoveQueue.add(listener);
		} else {
			listeners.remove(listener);
		}
	}

	protected synchronized void applyQueuedListeners() {

		if (listenerAddQueue.size() > 0) {
			listeners.addAll(listenerAddQueue);
			listenerAddQueue.clear();
		}

		if (listenerRemoveQueue.size() > 0) {
			listeners.removeAll(listenerRemoveQueue);
			listenerRemoveQueue.clear();
		}

	}

	// ========================================================================
	// Async Tasks

	private class CheckConnectionTask extends AsyncTask<Void, Void, CheckConnectionResult> {

		@Override
		protected CheckConnectionResult doInBackground(Void... params) {
			User currentlyActiveUser = getLastActiveUserFromDatabase();

			CheckConnectionResult result = new CheckConnectionResult();

			if (currentlyActiveUser != null) {
				String server = currentlyActiveUser.getCompany().getServer();

				FLLogger.d(TAG, "Checking connection to: " + server);
				int connectionStatus = usersRDAO.getCommunicator().testConnection(server);

				result.connectionStatus = connectionStatus;
				result.errorMessage = null;

				if (connectionStatus == HttpCommunicator.STATUS_CONNECTED) {
					FLLogger.d(TAG, "Connected");
				} else {
					FLLogger.d(TAG, "Disconnected");
				}

			} else {
				result.connectionStatus = HttpCommunicator.STATUS_ERROR_ON_CONNECT;
				result.errorMessage = "No active user yet";
			}

			for (CallbackCommand<CheckConnectionResult> callback : checkConnectionbackList) {
				if (callback != null) {
					callback.execute(result);
				}
			}

			checkConnectionbackList.clear();

			return result;

		}

		@Override
		protected void onPostExecute(CheckConnectionResult result) {
			super.onPostExecute(result);

			isCurrentlyCheckingServerConnection = false;

			for (CallbackCommand<CheckConnectionResult> callback : checkConnectionMainThreadCallbackList) {
				if (callback != null) {
					callback.execute(result);
				}
			}

			checkConnectionMainThreadCallbackList.clear();

		}

	}

	// ========================================================================
	// Interfaces

	public static interface SessionManagerEventListener {
		public void onModeChanged(Mode mode);

		public void onSessionLost();
	}

	// ============================================================================
	// Exceptions

	public static final class LoginException extends Exception {

		private static final long serialVersionUID = -1799369965596786842L;

		private LoginFields[] affectedFields;
		private APIResponse response;

		public LoginException(APIResponse response) {
			super(response.getErrorMessage());
		}

		public LoginException(String message, Throwable t) {
			super(message, t);
		}

		public LoginException(String message, LoginFields[] affectedFields) {
			super(message);
		}

		public LoginFields[] getAffectedFields() {
			return this.affectedFields;
		}

		public void setAffectedFields(LoginFields[] affectedFields) {
			this.affectedFields = affectedFields;
		}

		public APIResponse getAPIResponse() {
			return this.response;
		}

	}

	public static final class UserNotSavedOnDeviceException extends Exception {

		private static final long serialVersionUID = 3901545589995827721L;

		public UserNotSavedOnDeviceException() {
			super(
			        "The currently logged in user is not yet registered on this device, please login again");
		}

	}

	// ========================================================================
	// Getter & Setter Methods

	/**
	 * Searches the database for the last active user, if no such user is found, this
	 * method returns null.
	 * 
	 * @return
	 */
	public User getLastActiveUserFromDatabase() {

		usersLDAO.open();
		User lastActiveUser = usersLDAO.getActiveUser();
		usersLDAO.close();

		return lastActiveUser;

	}

	public Mode getMode() {
		return mode;
	}

	public int getConnectivityType() {
		return connectivityType;
	}

	public boolean isAuthenticated() {
		return isAuthenticated;
	}

	public synchronized boolean isCurrentlyCheckingServerConnection() {
		return isCurrentlyCheckingServerConnection;
	}
}
